<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>校园健康数据处理系统</title>
    <style>
        :root {
            --bg: #f6f8fb;
            --card-bg: #ffffff;
            --accent: #1f2a44;
            --accent-light: #e3e8f4;
            --text-main: #1f2937;
            --text-muted: #6b7280;
            --border: #d8dee9;
            --success: #1fa37b;
            --warning: #f97316;
            --danger: #ef4444;
        }

        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            font-family: "Segoe UI", "PingFang SC", "Hiragino Sans GB", sans-serif;
            background: radial-gradient(circle at top, #eef2f7, var(--bg));
            color: var(--text-main);
        }

        .container {
            max-width: 100%;
            margin: 20px auto 64px;
            padding: 0 24px 24px;
            display: grid;
            gap: 24px;
        }

        .card {
            background: var(--card-bg);
            border-radius: 18px;
            box-shadow: 0 14px 38px -26px rgba(23, 37, 84, 0.35);
            padding: 24px;
        }

        .upload-card {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 32px;
            flex-wrap: wrap;
            height: 280px;
        }

        .upload-card .intro {
            max-width: 520px;
        }

        .upload-card h2 {
            margin: 0 0 12px;
            font-size: 1.6rem;
            color: var(--accent);
        }

        .upload-card p {
            margin: 0 0 14px;
            color: var(--text-muted);
            line-height: 1.6;
        }

        .upload-control {
            display: inline-flex;
            align-items: center;
            gap: 14px;
        }

        .upload-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 12px 22px;
            border-radius: 12px;
            background: var(--accent);
            color: #fff;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s ease, box-shadow 0.2s ease;
            box-shadow: 0 12px 22px -18px rgba(17, 24, 39, 0.7);
        }

        .upload-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 16px 30px -20px rgba(15, 23, 42, 0.65);
        }

        .upload-btn input {
            position: absolute;
            clip: rect(0, 0, 0, 0);
            width: 1px;
            height: 1px;
            overflow: hidden;
        }

        .status {
            font-size: 0.95rem;
            color: #2563eb;
        }

        .hidden {
            display: none;
        }

        .controls-card {
            display: grid;
            gap: 18px;
        }

        .controls-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
            gap: 18px;
        }

        .control label {
            display: block;
            font-size: 0.85rem;
            margin-bottom: 6px;
            color: var(--text-muted);
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        .control input,
        .control select {
            width: 100%;
            padding: 10px 12px;
            border-radius: 10px;
            border: 1px solid var(--border);
            background: #f9fbff;
            font-size: 0.95rem;
            color: var(--text-main);
            transition: border-color 0.2s ease, box-shadow 0.2s ease;
        }

        .control input:focus,
        .control select:focus {
            outline: none;
            border-color: var(--accent);
            box-shadow: 0 0 0 3px rgba(31, 42, 68, 0.15);
        }

        .actions {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
            justify-content: flex-end;
        }

        .actions button {
            padding: 10px 18px;
            border-radius: 10px;
            border: none;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.95rem;
            color: #fff;
            background: linear-gradient(120deg, #1f2a44, #2d3b5f);
            transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
        }

        .actions button.secondary {
            background: linear-gradient(120deg, #4b5563, #6b7280);
        }

        .actions button:disabled {
            opacity: 0.35;
            cursor: not-allowed;
            box-shadow: none;
        }

        .actions button:not(:disabled):hover {
            transform: translateY(-1px);
            box-shadow: 0 12px 22px -16px rgba(31, 42, 68, 0.45);
        }

        .summary-grid {
            display: grid;
            max-width: 100%;
            margin: 0 auto;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 12px;
        }

        .summary-card {
            padding: 16px;
            border: 1px solid rgba(31, 42, 68, 0.05);
            border-radius: 16px;
            background: linear-gradient(160deg, #ffffff 60%, #f2f4f8);
        }

        .summary-card h3 {
            margin: 0 0 16px;
            font-size: 1.05rem;
            color: var(--accent);
            font-weight: 600;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .stat-line {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 10px;
            font-size: 0.95rem;
        }

        .stat-line strong {
            font-size: 1.2rem;
            color: var(--accent);
        }

        .stat-line small {
            margin-left: 6px;
            color: var(--text-muted);
            font-size: 0.8rem;
        }

        .summary-card ul {
            list-style: none;
            margin: 0;
            padding: 0;
            display: grid;
            gap: 6px;
        }

        .summary-card li {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px 10px;
            border-radius: 10px;
            background: rgba(31, 42, 68, 0.05);
            font-size: 0.9rem;
            color: var(--text-main);
        }

        .summary-card li strong {
            color: var(--accent);
        }

        .table-card {
            padding: 0;
            overflow: hidden;
            margin-top: 18px;
        }

        .table-responsive {
            width: 100%;
            overflow-x: auto;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            min-width: 960px;
        }

        thead {
            background: #1f2a44;
            color: #fff;
        }

        th,
        td {
            padding: 14px 16px;
            text-align: left;
            font-size: 0.95rem;
            border-bottom: 1px solid var(--accent-light);
        }

        th {
            text-transform: uppercase;
            font-size: 0.8rem;
            letter-spacing: 0.08em;
            font-weight: 500;
        }

        tr:hover td {
            background: rgba(31, 42, 68, 0.05);
        }

        .chip {
            display: inline-flex;
            align-items: center;
            padding: 2px 10px;
            margin: 2px;
            border-radius: 999px;
            background: var(--accent-light);
            color: var(--accent);
            font-size: 0.8rem;
            font-weight: 600;
        }

        .muted {
            color: var(--text-muted);
        }

        .empty-cell {
            text-align: center;
            color: var(--text-muted);
            font-style: italic;
        }

        .empty-notice {
            padding: 24px;
            text-align: center;
            color: var(--text-muted);
        }

        .subsystems {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
            gap: 24px;
            align-items: start;
        }

        .subsystem {
            display: grid;
            gap: 16px;
        }

        .modal {
            position: fixed;
            inset: 0;
            display: none;
            align-items: center;
            justify-content: center;
            background: rgba(15, 23, 42, 0.45);
            z-index: 999;
            padding: 24px;
        }

        .modal.show {
            display: flex;
        }

        .modal-content {
            width: 80%;
            max-width: none;
            background: linear-gradient(155deg, #ffffff 35%, #f6f7fb 100%);
            border-radius: 20px;
            box-shadow: 0 28px 64px -36px rgba(13, 23, 49, 0.6);
            overflow: hidden;
        }

        .modal-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 18px 24px;
            background: #f1f3f9;
            border-bottom: 1px solid rgba(31, 42, 68, 0.08);
        }

        .modal-header h3 {
            margin: 0;
            font-size: 1.1rem;
            color: var(--accent);
        }

        .modal-header button {
            border: none;
            background: transparent;
            font-size: 1.4rem;
            line-height: 1;
            cursor: pointer;
            color: var(--text-muted);
        }

        .modal-header button:hover {
            color: var(--accent);
        }

        .modal-body {
            padding: 0 0 24px;
            max-height: 72vh;
            overflow-y: auto;
            background: rgba(255, 255, 255, 0.6);
        }

        .comparison-wrapper {
            padding: 0 24px 24px;
            overflow: hidden;
        }

        .comparison-legend {
            display: flex;
            align-items: center;
            gap: 14px;
            padding: 18px 20px;
            margin: 0 24px;
            border-radius: 12px;
            background: rgba(31, 42, 68, 0.08);
            color: var(--text-muted);
            font-size: 0.9rem;
        }

        .comparison-legend span {
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }

        .comparison-scroll {
            margin: 18px 24px 0;
            border-radius: 14px;
            overflow: auto;
            background: rgba(255, 255, 255, 0.75);
            box-shadow: inset 0 0 0 1px rgba(31, 42, 68, 0.06);
        }

        .comparison-table {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
            min-width: 640px;
        }

        .comparison-table thead th:first-child,
        .comparison-table tbody th {
            position: sticky;
            left: 0;
            background: rgba(31, 42, 68, 0.08);
            z-index: 3;
        }

        .comparison-table th,
        .comparison-table td {
            padding: 12px 16px;
            text-align: left;
            border-bottom: 1px solid rgba(31, 42, 68, 0.08);
            font-size: 0.9rem;
        }

        .comparison-table tbody tr:nth-child(odd) {
            background: rgba(31, 42, 68, 0.035);
        }

        .diff-chip {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            min-width: 88px;
            padding: 4px 10px;
            border-radius: 999px;
            font-weight: 600;
            font-size: 0.85rem;
            letter-spacing: 0.02em;
        }

        .diff-positive {
            color: #0b7a41;
            background: rgba(15, 118, 110, 0.12);
        }

        .diff-negative {
            color: #b91c1c;
            background: rgba(220, 38, 38, 0.12);
        }

        .diff-zero {
            color: var(--accent);
            background: rgba(31, 42, 68, 0.12);
        }

        .upload-card .notice {
            color: #dc2626;
        }

        .modal-actions {
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .modal-export {
            padding: 8px 16px;
            border: none;
            border-radius: 999px;
            background: linear-gradient(120deg, #152e64, #4338ca);
            color: #fff;
            font-weight: 600;
            font-size: 0.9rem;
            cursor: pointer;
            box-shadow: 0 10px 22px -16px rgba(59, 130, 246, 0.65);
            transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
        }

        .modal-export:disabled {
            opacity: 0.4;
            cursor: not-allowed;
            box-shadow: none;
        }

        .modal-export:not(:disabled):hover {
            transform: translateY(-1px);
            box-shadow: 0 16px 28px -18px rgba(59, 130, 246, 0.6);
        }

        .floating-btn {
            position: fixed;
            width: 96%;
            text-align: center;
            right: 2%;
            bottom: 32px;
            z-index: 1000;
            padding: 14px 24px;
            border: none;
            border-radius: 999px;
            background: linear-gradient(120deg, #1f2a44, #2d3b5f);
            color: #fff;
            font-weight: 600;
            font-size: 1rem;
            cursor: pointer;
            box-shadow: 0 16px 32px -22px rgba(15, 23, 42, 0.6);
            transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
        }

        .floating-btn:disabled {
            cursor: not-allowed;
            opacity: 0.4;
            box-shadow: none;
        }

        .floating-btn:not(:disabled):hover {
            transform: translateY(-2px);
            box-shadow: 0 22px 38px -24px rgba(15, 23, 42, 0.6);
        }

        @media (max-width: 900px) {
            .upload-card {
                flex-direction: column;
                align-items: flex-start;
            }

            .summary-grid {
                grid-template-columns: 1fr;
            }

            .page-header {
                padding: 32px 18px 16px;
            }

            .page-header h1 {
                font-size: 2rem;
            }
        }

        body.modal-open {
            overflow: hidden;
        }
    </style>
</head>

<body>
    <main class="container">
        <div id="systemsContainer" class="subsystems"></div>
    </main>

    <button id="compareTrigger" class="floating-btn" type="button" disabled>对比诊断</button>

    <template id="systemTemplate">
        <section class="subsystem">
            <section class="card upload-card">
                <div class="intro">
                    <h2 data-role="title">日期</h2>
                    <p class="notice">1. 请将班级名称统一为“2.10 班”形式以便系统校验，否则将导致对比结果不准确<br>
                        2. 若原始表格内容不完整，可复制到全新的 xlsx文件后再导入，避免因加密或格式问题无法解析。</p>
                    <p class="status" data-role="status">尚未选择文件。</p>
                </div>
                <div class="upload-control">
                    <label class="upload-btn">
                        <input type="file" data-role="file-input" accept=".xlsx,.xls" />
                        <span>选择 XLSX 文件</span>
                    </label>
                </div>
            </section>

            <section class="hidden" data-role="dashboard">
                <section class="summary-grid" data-role="summary">
                    <div class="summary-card">
                        <h3>提示</h3>
                        <p class="muted">请先导入数据表。</p>
                    </div>
                </section>

                <section class="card table-card">
                    <div class="table-responsive">
                        <table>
                            <thead>
                                <tr>
                                    <th>姓名</th>
                                    <th>性别</th>
                                    <th>班级</th>
                                    <th>症状表现</th>
                                    <th>处置及去向</th>
                                    <th>就诊医院</th>
                                    <th>诊断结果</th>
                                </tr>
                            </thead>
                            <tbody data-role="table-body">
                                <tr>
                                    <td colspan="7" class="empty-cell">请上传 xlsx 数据开始分析。</td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </section>
            </section>
        </section>
    </template>

    <div id="comparisonModal" class="modal">
        <div class="modal-content">
            <header class="modal-header">
                <h3>班级诊断分布变化</h3>
                <div class="modal-actions">
                    <button type="button" class="modal-export" data-role="export-comparison" disabled>导出结果</button>
                    <button type="button" data-role="close-modal">&times;</button>
                </div>
            </header>
            <div class="modal-body">
                <div id="comparisonBody"></div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
    <script>
        (() => {
            const systemsContainer = document.getElementById('systemsContainer');
            const template = document.getElementById('systemTemplate');
            const symptomDefs = [
                { key: 'fever', label: '发热', col: 4 },
                { key: 'cough', label: '咳嗽', col: 5 },
                { key: 'runnyNose', label: '流涕', col: 6 },
                { key: 'weakness', label: '无力', col: 7 },
                { key: 'vomit', label: '呕吐', col: 8 },
                { key: 'otherSymptom', label: '其他', col: 9 }
            ];

            const diagnosisDefs = [
                { key: 'commonCold', label: '普通感冒', col: 12 },
                { key: 'flu', label: '流感或甲流', col: 13 },
                { key: 'chickenpox', label: '水痘', col: 14 },
                { key: 'mumps', label: '流腮', col: 15 },
                { key: 'scarlet', label: '猩红热', col: 16 },
                { key: 'hfmd', label: '手足口', col: 17 },
                { key: 'noro', label: '诺如病毒', col: 18 },
                { key: 'otherDiag', label: '其他', col: 19 },
                { key: 'suspected', label: '疑似传染病例', col: 20 }
            ];

            const diagKeysForDiff = diagnosisDefs.filter((def) => def.key !== 'suspected');
            const systemData = new Map();
            const modal = document.getElementById('comparisonModal');
            const comparisonBody = document.getElementById('comparisonBody');
            const compareTrigger = document.getElementById('compareTrigger');
            const closeModalBtn = modal.querySelector('[data-role="close-modal"]');
            const exportModalBtn = modal.querySelector('[data-role="export-comparison"]');
            closeModalBtn.addEventListener('click', hideModal);
            exportModalBtn.addEventListener('click', exportComparison);
            modal.addEventListener('click', (event) => {
                if (event.target === modal) hideModal();
            });
            document.addEventListener('keydown', (event) => {
                if (event.key === 'Escape') hideModal();
            });

            const systemConfigs = [
                { key: 'prev', title: '前日 数据' },
                { key: 'next', title: '后日 数据' }
            ];

            systemConfigs.forEach(({ key, title }) => {
                const fragment = template.content.cloneNode(true);
                const root = fragment.querySelector('.subsystem');
                root.querySelector('[data-role="title"]').textContent = title;
                systemsContainer.appendChild(fragment);
                initSubsystem(root, title, key);
            });

            function initSubsystem(root, label, key) {
                const fileInput = root.querySelector('[data-role="file-input"]');
                const statusText = root.querySelector('[data-role="status"]');
                const dashboard = root.querySelector('[data-role="dashboard"]');
                const summaryGrid = root.querySelector('[data-role="summary"]');
                const tableBody = root.querySelector('[data-role="table-body"]');
                let records = [];

                fileInput.addEventListener('change', (evt) => {
                    const file = evt.target.files[0];
                    if (!file) {
                        statusText.textContent = '尚未选择文件。';
                        return;
                    }
                    statusText.textContent = `正在读取 ${file.name} ...`;
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        try {
                            const data = new Uint8Array(e.target.result);
                            const workbook = XLSX.read(data, { type: 'array' });
                            const sheetName = workbook.SheetNames[0];
                            const sheet = workbook.Sheets[sheetName];
                            records = parseSheet(sheet);
                            statusText.textContent = records.length
                                ? `已载入 ${file.name}，共 ${records.length} 条记录。`
                                : `已读取 ${file.name}，但未解析到有效记录。`;
                            dashboard.classList.remove('hidden');
                            if (!records.length) {
                                summaryGrid.innerHTML = `<article class="summary-card"><h3>${escapeHtml(label)} 数据概览</h3><p class="muted">暂无有效记录。</p></article>`;
                                tableBody.innerHTML = `<tr><td colspan="7" class="empty-cell">暂无数据。</td></tr>`;
                                updateSystemData(key, records);
                                return;
                            }
                            renderSummary(records, summaryGrid);
                            renderTable(records, tableBody);
                            updateSystemData(key, records);
                        } catch (error) {
                            console.error(error);
                            statusText.textContent = '解析文件时发生错误，请检查表格结构。';
                            summaryGrid.innerHTML = `<article class="summary-card"><h3>解析失败</h3><p class="muted">${escapeHtml(error.message || '未知错误')}</p></article>`;
                            tableBody.innerHTML = `<tr><td colspan="7" class="empty-cell">解析失败，无法展示数据。</td></tr>`;
                            systemData.delete(key);
                        }
                    };
                    reader.readAsArrayBuffer(file);
                });
            }

            function parseSheet(sheet) {
                const records = [];
                if (!sheet || !sheet['!ref']) return records;
                const range = XLSX.utils.decode_range(sheet['!ref']);
                for (let r = 6; r <= range.e.r; r++) {
                    const name = readText(sheet, r, 1);
                    if (!name || ['张三', '李四'].includes(name)) continue;
                    const gender = readText(sheet, r, 2);
                    const classNameRaw = readText(sheet, r, 3);
                    const className = normalizeClassName(classNameRaw);
                    const disposal = readText(sheet, r, 10);
                    const hospital = readText(sheet, r, 11);

                    const symptomFlags = {};
                    const symptomList = [];
                    symptomDefs.forEach((def) => {
                        const flag = normalizeFlag(readText(sheet, r, def.col));
                        symptomFlags[def.key] = flag;
                        if (flag) symptomList.push(def.label);
                    });

                    const diagnosisFlags = {};
                    const diagnosisList = [];
                    diagnosisDefs.forEach((def) => {
                        const flag = normalizeFlag(readText(sheet, r, def.col));
                        diagnosisFlags[def.key] = flag;
                        if (flag) diagnosisList.push(def.label);
                    });
                    if (!diagnosisList.length) {
                        diagnosisFlags.otherDiag = 1;
                        const otherLabel = diagnosisDefs.find((def) => def.key === 'otherDiag')?.label || '其他';
                        diagnosisList.push(otherLabel);
                    }

                    records.push({
                        name,
                        gender,
                        className,
                        disposal,
                        hospital,
                        symptomFlags,
                        symptomList,
                        diagnosisFlags,
                        diagnosisList
                    });
                }
                return records;
            }

            function renderSummary(records, summaryGrid) {
                if (!records.length) {
                    summaryGrid.innerHTML = `<article class="summary-card"><h3>数据概览</h3><p class="muted">暂无有效记录。</p></article>`;
                    return;
                }

                const total = records.length;
                const symptomatic = records.filter((item) => item.symptomList.length > 0).length;
                const suspected = records.filter((item) => item.diagnosisFlags.suspected).length;

                const symptomCounts = symptomDefs
                    .map((def) => ({
                        label: def.label,
                        count: records.reduce((sum, rec) => sum + (rec.symptomFlags[def.key] ? 1 : 0), 0)
                    }))
                    .filter((item) => item.count > 0);

                const diagnosisCounts = diagnosisDefs
                    .map((def) => ({
                        key: def.key,
                        label: def.label,
                        count: records.reduce((sum, rec) => sum + (rec.diagnosisFlags[def.key] ? 1 : 0), 0)
                    }));

                summaryGrid.innerHTML = `
					<article class="summary-card">
						<h3>总体概览</h3>
						<div class="stat-line"><span>记录总数</span><strong>${total}</strong></div>
						<div class="stat-line"><span>有症状</span><strong>${symptomatic}</strong><small>${percent(symptomatic, total)}</small></div>
						<div class="stat-line"><span>疑似传染病例</span><strong style="color: ${suspected ? 'var(--danger)' : 'var(--accent)'}">${suspected}</strong><small>${percent(suspected, total)}</small></div>
					</article>
					<article class="summary-card">
						<h3>症状分布</h3>
						<ul>
							${symptomCounts.length ? symptomCounts.map((item) => `<li><span>${item.label}</span><strong>${item.count}</strong></li>`).join('') : '<li><span>无相关记录</span><strong>0</strong></li>'}
						</ul>
					</article>
					<article class="summary-card">
						<h3>诊断分布</h3>
						<ul>
							${diagnosisCounts.length ? diagnosisCounts.map((item) => `<li><span>${item.label}</span><strong>${item.count}</strong></li>`).join('') : '<li><span>无相关记录</span><strong>0</strong></li>'}
						</ul>
					</article>
				`;
            }

            function renderTable(records, tableBody) {
                if (!records.length) {
                    tableBody.innerHTML = `<tr><td colspan="7" class="empty-cell">暂无数据。</td></tr>`;
                    return;
                }
                tableBody.innerHTML = records.map((record) => {
                    const symptomHtml = record.symptomList.length
                        ? record.symptomList.map((label) => `<span class="chip">${escapeHtml(label)}</span>`).join('')
                        : '<span class="chip">其他</span>';

                    const diagnosisHtml = record.diagnosisList.length
                        ? record.diagnosisList.map((label) => `<span class="chip">${escapeHtml(label)}</span>`).join('')
                        : '<span class="muted">—</span>';

                    return `
						<tr>
							<td>${escapeHtml(record.name)}</td>
							<td>${escapeHtml(record.gender)}</td>
							<td>${escapeHtml(record.className)}</td>
							<td>${symptomHtml}</td>
							<td>${escapeHtml(record.disposal)}</td>
							<td>${escapeHtml(record.hospital)}</td>
							<td>${diagnosisHtml}</td>
						</tr>
					`;
                }).join('');
            }

            function updateSystemData(key, records) {
                if (records && records.length) {
                    systemData.set(key, records);
                } else {
                    systemData.delete(key);
                }
                updateComparisonState();
            }

            function updateComparisonState() {
                const beforeData = systemData.get('prev');
                const afterData = systemData.get('next');
                const ready = Boolean(beforeData?.length && afterData?.length);
                compareTrigger.disabled = !ready;
                compareTrigger.classList.toggle('active', ready);
                if (!ready) hideModal();
            }

            compareTrigger.addEventListener('click', () => {
                if (compareTrigger.disabled) return;
                const beforeData = systemData.get('prev');
                const afterData = systemData.get('next');
                if (!beforeData?.length || !afterData?.length) return;
                renderComparison(beforeData, afterData);
            });

            function maybeShowComparison() {
                const beforeData = systemData.get('prev');
                const afterData = systemData.get('next');
                if (!beforeData?.length || !afterData?.length) return;
                renderComparison(beforeData, afterData);
            }

            let comparisonSnapshot = null;

            function renderComparison(beforeRecords, afterRecords) {
                const beforeMap = aggregateByClass(beforeRecords);
                const afterMap = aggregateByClass(afterRecords);
                const allClasses = new Set([...beforeMap.keys(), ...afterMap.keys()]);
                if (!allClasses.size) {
                    comparisonSnapshot = null;
                    exportModalBtn.disabled = true;
                    comparisonBody.innerHTML = '<p class="muted">暂无班级数据可对比。</p>';
                    modal.classList.add('show');
                    document.body.classList.add('modal-open');
                    return;
                }
                const rowsData = Array.from(allClasses)
                    .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'))
                    .map((cls) => {
                        const beforeCounts = beforeMap.get(cls) ?? createEmptyCounts();
                        const afterCounts = afterMap.get(cls) ?? createEmptyCounts();
                        const diffs = diagKeysForDiff.map((def) => (afterCounts[def.key] ?? 0) - (beforeCounts[def.key] ?? 0));
                        return { className: cls, diffs };
                    });
                comparisonSnapshot = {
                    headers: diagKeysForDiff.map((def) => def.label),
                    rows: rowsData
                };
                const headerCells = diagKeysForDiff.map((def) => `<th>${def.label}</th>`).join('');
                const rows = rowsData.map((row) => {
                    const cells = row.diffs
                        .map((diff, idx) => {
                            const beforeVal = (aggregateByClass(beforeRecords).get(row.className) ?? createEmptyCounts())[diagKeysForDiff[idx].key] ?? 0;
                            const afterVal = (aggregateByClass(afterRecords).get(row.className) ?? createEmptyCounts())[diagKeysForDiff[idx].key] ?? 0;
                            const diffClass = diff > 0 ? 'diff-positive' : diff < 0 ? 'diff-negative' : 'diff-zero';
                            return `<td><span class="diff-chip ${diffClass}">${afterVal}-${beforeVal}=${diff}</span></td>`;
                        })
                        .join('');
                    return `<tr><th>${escapeHtml(row.className)}</th>${cells}</tr>`;
                }).join('');
                comparisonBody.innerHTML = `
		<div class="comparison-wrapper">
			<div class="comparison-legend">
				<span><span class="diff-chip diff-positive">↑</span> 后日比前日上升</span>
				<span><span class="diff-chip diff-negative">↓</span> 后日比前日下降</span>
				<span><span class="diff-chip diff-zero">=</span> 无变化</span>
			</div>
			<div class="comparison-scroll">
				<table class="comparison-table">
					<thead>
						<tr>
							<th>班级</th>
							${headerCells}
						</tr>
					</thead>
					<tbody>
						${rows}
					</tbody>
				</table>
			</div>
		</div>
	`;
                exportModalBtn.disabled = false;
                modal.classList.add('show');
                document.body.classList.add('modal-open');
            }

            function aggregateByClass(records) {
                const bucket = new Map();
                records.forEach((record) => {
                    const classKey = record.className || '未注明班级';
                    if (!bucket.has(classKey)) bucket.set(classKey, createEmptyCounts());
                    const counts = bucket.get(classKey);
                    diagKeysForDiff.forEach((def) => {
                        if (record.diagnosisFlags[def.key]) counts[def.key] += 1;
                    });
                });
                return bucket;
            }

            function createEmptyCounts() {
                const counts = {};
                diagKeysForDiff.forEach((def) => {
                    counts[def.key] = 0;
                });
                return counts;
            }

            function readText(sheet, r, c) {
                const cell = sheet[XLSX.utils.encode_cell({ c, r })];
                if (!cell) return '';
                if (cell.t === 's' || cell.t === 'str') return String(cell.v).trim();
                if (cell.t === 'n') return cell.w ? String(cell.w).trim() : String(cell.v).trim();
                if (cell.t === 'b') return cell.v ? '1' : '';
                return cell.v ? String(cell.v).trim() : '';
            }

            function normalizeFlag(value) {
                const str = String(value || '').trim();
                if (!str || str === '无' || str === '0') return 0;
                return 1;
            }

            function percent(count, total) {
                if (!total) return '0%';
                return `${Math.round((count / total) * 100)}%`;
            }

            function escapeHtml(value) {
                return String(value || '')
                    .replace(/&/g, '&amp;')
                    .replace(/</g, '&lt;')
                    .replace(/>/g, '&gt;')
                    .replace(/"/g, '&quot;')
                    .replace(/'/g, '&#39;');
            }

            function normalizeClassName(value) {
                const raw = (value ?? '').toString();
                if (!raw.trim()) return '';
                const merged = convertChineseNumerals(raw)
                    .replace(/\s+/g, '')
                    .replace(/[．·]/g, '.')
                    .replace(/[、，,]/g, '.')
                    .replace(/[()（）]/g, '.');
                const tokens = merged.match(/\d+/g) || [];
                let gradeNum;
                let classNum;
                if (tokens.length >= 2) {
                    gradeNum = parseInt(tokens[0], 10);
                    classNum = parseInt(tokens[1], 10);
                } else if (tokens.length === 1) {
                    const compact = tokens[0];
                    if (compact.length >= 2) {
                        gradeNum = parseInt(compact.slice(0, 1), 10);
                        classNum = parseInt(compact.slice(1), 10);
                    }
                }
                if (!Number.isFinite(gradeNum) || !Number.isFinite(classNum) || gradeNum <= 0 || classNum <= 0) {
                    return raw.trim();
                }
                return `${gradeNum}.${String(classNum).padStart(2, '0')} 班`;
            }

            function convertChineseNumerals(str) {
                return (str || '').replace(/[一二三四五六七八九十零〇两]+/g, chineseSegmentToNumber);
            }

            function chineseSegmentToNumber(seg) {
                const map = { '零': 0, '〇': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 };
                if (seg === '十') return '10';
                const tenIdx = seg.indexOf('十');
                if (tenIdx !== -1) {
                    const left = seg.slice(0, tenIdx);
                    const right = seg.slice(tenIdx + 1);
                    const tens = left ? (map[left] ?? NaN) : 1;
                    const ones = right ? (map[right] ?? NaN) : 0;
                    if (!Number.isNaN(tens) && !Number.isNaN(ones)) {
                        return String(tens * 10 + ones);
                    }
                }
                let result = '';
                for (const ch of seg) {
                    result += map[ch] !== undefined ? map[ch] : ch;
                }
                return result;
            }

            function hideModal() {
                modal.classList.remove('show');
                document.body.classList.remove('modal-open');
            }

            function exportComparison() {
                if (!comparisonSnapshot || !comparisonSnapshot.rows.length) return;
                const exportRows = comparisonSnapshot.rows.map((row) => {
                    const record = { 班级: row.className };
                    comparisonSnapshot.headers.forEach((label, idx) => {
                        record[label] = row.diffs[idx];
                    });
                    return record;
                });
                const worksheet = XLSX.utils.json_to_sheet(exportRows);
                const workbook = XLSX.utils.book_new();
                XLSX.utils.book_append_sheet(workbook, worksheet, '班级对比');
                // 文件名加上有时间具体到秒
                const filename = `班级诊断对比_${new Date().toISOString().replace(/[:\-T]/g, '').slice(0, 14)}.xlsx`;
                XLSX.writeFile(workbook, filename);
            }
        })();
    </script>
</body>

</html>